Event-Based Programming Without Inversion of Control
نویسندگان
چکیده
class Actor extends Thread { private var mailbox: List[Any] def !(msg: Any) = ... def receive[a](f: PartialFunction[Any, a]): a = ... ... } The ! method is used to send a message to an actor. The send syntax a!m is simply an abbreviation of the method call a.!(m), just like x+y in Scala is an abbreviation for x.+(y). The method does two things. First, it enqueues the message argument in the actor's mailbox, which is represented as a private eld of type List[Any]. Second, if the receiving actor is currently suspended in a receive that could handle the sent message, the execution of the actor is resumed. The receive { ... } construct is more interesting. Here, the pattern matching expression inside the braces is treated in Scala as a rst-class object that is passed as an argument to the receive method. The argument's type is an instance of PartialFunction, which is a subclass of Function1, the class of unary functions. The two classes are de ned as follows. abstract class Function1[-a,+b] { def apply(x: a): b } abstract class PartialFunction[-a,+b] extends Function1[a,b] { def isDefinedAt(x: a): boolean }class Function1[-a,+b] { def apply(x: a): b } abstract class PartialFunction[-a,+b] extends Function1[a,b] { def isDefinedAt(x: a): boolean } So we see that functions are objects which have an apply method. Partial functions are objects which have in addition a method isDefinedAt which can be used to nd out whether a function is de ned at a given value. Both classes are parameterized; the rst type parameter a indicates the function's argument type and the second type parameter b indicates its result type4. A pattern matching expression { case p1 => e1; ...; case pn => en } is then a partial function whose methods are de ned as follows. { The isDefinedAt method returns true if one of the patterns pi matches the argument, false otherwise. { The apply method returns the value ei for the rst pattern pi that matches its argument. If none of the patterns match, a MatchError exception is thrown. The two methods are used in the implementation of receive as follows. First, messages m in the mailbox are scanned in the order they appear. If receive's argument f is de ned for some of the messages, that message is removed from the mailbox and f is applied to it. On the other hand, if f.isDefinedAt(m) is false for every message in the mailbox, the thread associated with the actor is suspended. This sums up the essential implementation of thread-based actors. There is also some other functionality in Scala's actor libraries which we have not covered. For instance, there is a method receiveWithin which can be used to specify a time span in which a message should be received allowing an actor to timeout while waiting for a message. Upon timeout the action associated with a special TIMEOUT() pattern is red. Timeouts can be used to suspend an actor, completely ush the mailbox, or to implement priority messages [4]. Thread-based actors are useful as a higher-level abstraction of threads, which replace error-prone shared memory accesses and locks by asynchronous message passing. However, like threads they incur a performance penalty on standard platforms such as the JVM, which prevents scalability. In the next section we show how the actor model can be changed so that actors become disassociated from threads. 4 Parameters can carry + or variance annotations which specify the relationship between instantiation and subtyping. The -a, +b annotations indicate that functions are contravariant in their argument and covariant in their result. In other words Function1[X1, Y1] is a subtype of Function1[X2, Y2] if X2 is a subtype of X1 and Y1 is a subtype of Y2. 3 Recomposing Actors Logically, an actor is not bound to a thread of execution. Nevertheless, virtually all implementations of actor models associate a separate thread or even an operating system process with each actor [8,29,9,30]. In Scala, thread abstractions of the standard library are mapped onto the thread model and implementation of the corresponding target platform, which at the moment consists of the JVM and Microsoft's CLR [15]. To overcome the resulting problems with scalability, we propose an event-based implementation where (1) actors are thread-less, and (2) computations between two events are allowed to run to completion. An event in our library corresponds to the arrival of a new message in an actor's mailbox. 3.1 Execution Example First, we want to give an intuitive explanation of how our event-based implementation works. For this, we revisit our counter example from section 2. Let c be a new instance of a lockable counter (with an empty mailbox). After starting c it immediately blocks, waiting for a matching message. Consider the case where another actor p sends the message Lock(p) to c (c ! Lock(p)). Because the arrival of this Lock message enables c to continue, send transfers control to c. c resumes the receive statement that caused it to block. Instead of executing the receiving actor on its own thread, we reuse the sender's thread. According to the semantics of receive, the new message is selected and removed from the mailbox because it matches the rst case of the outer receive. Then, the corresponding action is executed with the pattern variables bound to the constituents of the matched message: { case Incr() => loop(value + 1) case Value(a) => a ! value; loop(value) case Lock(a) => a ! value receive { case UnLock(v) => loop(v) } case _ => loop(value) }.apply(Lock(p)) Intuitively, this reduces to p ! value receive { case UnLock(v) => loop(v) } After executing the message send p ! value, the call to receive blocks as there are no other messages in c's mailbox. Remember that we are still inside p's original message send (i.e. the send did not return, yet). Thus, blocking the current thread (e.g., by issuing a call to wait()) would also block p. This is illegal because in our programming model the send operation (!) has a non-blocking semantics. Instead, we need to suspend c in such a way that allows p to continue. For this, inside the (logically) blocking receive, rst, we remember the rest of c's computation. In this case, it su ces to save the closure of receive { case UnLock(v) => loop(v) } Second, to let p's call of the send operation return, we need to unwind the runtime stack up to the point where control was transferred to c. We do this by throwing a special exception. The ! method catches this exception and returns normally, keeping its non-blocking semantics. In general, though, it is not su cient to save a closure to capture the rest of the computation of an actor. For example, consider an actor executing the following statements: val x = receive { case y => f(y) } g(x) Here, receive produces a value which is then passed to a function. Assume receive blocks. Remember that we would need to save the rest of the computation inside the blocking receive. To save information about statements following receive, we would need to save the call-stack, or capture a ( rst-class) continuation. Virtual machines such as the JVM provide no means for explicit stack management, mainly because of security reasons. Thus, languages implementing rst-class continuations have to simulate the run-time stack on the heap which poses serious performance problems [7]. Moreover, programming tools such as debuggers and pro lers rely on run-time information on the native VM stack which they are unable to nd if the stack that programs are using is allocated on the heap. Consequently, existing tools cannot be used with programs compiled using a heap-allocated stack. Thus, most ports of languages with continuation support (e.g. Scheme [18], Ruby [24]) onto non-cooperative virtual machines abandon rst-class continuations altogether (e.g. JScheme [2], JRuby5). Scala does not support rst-class continuations either, primarily because of compatibility and interoperability issues with existing Java code. To conclude, managing information about statements following a call to receive would require changes either to the compiler or the VM. Following our rationale for a library-based approach, we want to avoid those changes. Instead, we require that receive never returns normally. Thus, managing information about succeeding statements is unnecessary. Moreover, we can enforce this \no-return" property at compile time through Scala's type system which 5 See http://jruby.sourceforge.net/. states that statements following calls to functions (or methods) with return type Nothing will never get executed (\dead code") [26]. Note that returning by throwing an exception is still possible. In fact, as already mentioned above, our implementation of receive relies on it. Using a non-returning receive, the above example could be coded like this: receive { case y => x = f(y); g(x) } Basically, the rest of the actor's computation has to be called at the end of each case inside the argument function of receive (\continuation passing" style). 3.2 Single-Threaded Actors As we want to avoid inversion of control receive will (conceptually) be executed at the expense of the sender. If all actors are running on a single thread, sending a message to an actor A will resume the execution of receive which caused A to suspend. The code below shows a simpli ed implementation of the send operation for actors that run on a single thread: def !(msg: Any): unit = { mailbox += msg if (continuation != null && continuation.isDefinedAt(msg)) try { receive(continuation) } catch { case Done => // do nothing } } The sent message is appended to the mailbox of the actor which is the target of the send operation. Let A denote the target actor. If the continuation attribute is set to a non-null value then A is suspended waiting for an appropriate message (otherwise, A did not execute a call to receive, yet). As continuation refers to (the closure of) the partial function with which the last blocking receive was called, we can test if the newly appended message allows A to continue. Note that if, instead, we would save receive(f) as continuation for a blocking receive(f) we would not be able to test this but rather had to blindly call the continuation. If the newly appended message would not match any of the de ned patterns, receive would go through all messages in the mailbox again trying to nd the rst matching message. Of course, the attempt would be in vain as only the newly appended message could have enabled A to continue. If A is able to process the newly arrived message we let A continue until it blocks on a nested receive(g) or nishes its computation. In the former case, we rst save the closure of g as A's continuation. Then, the send operation that originated A's execution has to return because of its non-blocking semantics. For this, the blocking receive throws a special exception of type Done (see below) which is caught in the send method (!). Technically, this trick unwinds the callstack up to the point where the message send transferred control to A. Thus, to complete the explanation of how the implementation of the send operation works, we need to dive into the implementation of receive. The receive method selects messages from an actor's mailbox and is responsible for saving the continuation as well as abandoning the evaluation context: def receive(f: PartialFunction[Any, unit]): Nothing = { mailbox.dequeueFirst(f.isDefinedAt) match { case Some(msg) => continuation = null f(msg) case None => continuation = f } throw new Done } Naturally, we dequeue the rst message in our mailbox which matches one of the cases de ned by the partial function which is provided as an argument to receive. Note that f.isDefinedAt has type Any => boolean. As the type of the resulting object is Option[Any] which has two cases de ned, we can select between these cases using pattern matching. When there was a message dequeued we rst reset the saved continuation. This is necessary to prevent a former continuation to be called multiple times when there is a send to the current actor inside the call f(msg). If we didn't nd a matching message in the mailbox, we remember the continuation which is the closure of f. In both cases we need to abandon the evaluation context by throwing a special exception of type Done, so the sender which originated the call to receive can continue normally (see above). 3.3 Multi-Threaded Actors To leverage the increasingly important class of multi-core processors (and also multi-processors) we want to execute concurrent activities on multiple threads. We rely on modern VM implementations to execute concurrent VM threads on multiple processor cores in parallel. A scheduler decides how many threads to spend for a given workload of concurrent actors, and, naturally, implements a speci c scheduling strategy. Because of its asychronous nature, a message send introduces a concurrent activity, namely the resumption of a previously suspended actor. We encapsulate this activity in a task item which gets submitted to the scheduler (in a sense this is a rescheduling
منابع مشابه
Inversion of Gravity Data by Constrained Nonlinear Optimization based on nonlinear Programming Techniques for Mapping Bedrock Topography
A constrained nonlinear optimization method based on nonlinear programming techniques has been applied to map geometry of bedrock of sedimentary basins by inversion of gravity anomaly data. In the inversion, the applying model is a 2-D model that is composed of a set of juxtaposed prisms whose lower depths have been considered as unknown model parameters. The applied inversion method is a nonli...
متن کاملDeclarative Events for Object-Oriented Programming
In object-oriented designs inversion of control is achieved by an event-driven programming style based on imperatively triggered events. An alternative approach can be found in aspect-oriented programming, which de nes events as declarative queries over implicitly available events. This helps to localize de nition of events and avoid preplanning, but lacks a clean integration with object-orient...
متن کاملAn Object-Oriented Programming Model for Event-Based Actors
Actors are concurrent processes which communicate through asynchronous message passing. Most existing actor languages and libraries implement actors using virtual machine or operating system threads. The resulting actor abstractions are rather heavyweight, both in terms of memory consumption and synchronization. Consequently, their systems are not suited for resource-constrained devices or high...
متن کاملAdaptive attitude controller of a reentry vehicles based on Back-stepping Dynamic inversion method
This paper presents an attitude control algorithm for a Reusable Launch Vehicle (RLV) with a low lift/drag ratio (L/D < 0.5), in the presence of external disturbances, model uncertainties, control output constraints and the thruster model. The main novelty of proposed control strategy is a new combination of the attitude control methods included backstepping, dynamic inversion and adaptive cont...
متن کاملActive Objects Provide Robust Event-driven Applications
The non-determinism inherent in event-driven systems encompassing both networked applications and interactive applications, makes these applications difficult to develop and maintain, despite the availability of powerful libraries. One reason for this is the so-called inversion of control needed to dispatch events to listener objects provided by the application. In this paper we will argue that...
متن کاملA new stochastic 3D seismic inversion using direct sequential simulation and co-simulation in a genetic algorithm framework
Stochastic seismic inversion is a family of inversion algorithms in which the inverse solution was carried out using geostatistical simulation. In this work, a new 3D stochastic seismic inversion was developed in the MATLAB programming software. The proposed inversion algorithm is an iterative procedure that uses the principle of cross-over genetic algorithms as the global optimization techniqu...
متن کاملذخیره در منابع من
با ذخیره ی این منبع در منابع من، دسترسی به آن را برای استفاده های بعدی آسان تر کنید
عنوان ژورنال:
دوره شماره
صفحات -
تاریخ انتشار 2006